package model.symbol;

import event.SymbolChangeEvent;
import event.SymbolRenameEvent;
import model.caret.TextCaret;
import model.code.line.NameLine;
import model.code.line.SingleLine;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;

/**
 * This class is a set of Symbols all with the same scope.
 *
 * @author Neil Dickson
 */
public class Scope {
	private	Scope								parent;
	private	int									type;
	private	HashMap<String,ArrayList<Symbol>>	symbolsByName;
	private HashMap<Long,Symbol>				symbolsByID;

	private	ArrayList<ScopeObserver>			observers;
	private	boolean								isNotificationEnabled;

	private	static Symbol						beingRenamed = null;
	private	static final ArrayList<TextCaret>	addedRenameReferences = new ArrayList<TextCaret>();

	private static int	NEXT_UNIQUE_ID = 0;

	public static final int TYPE_LANGUAGE	= 0;
	public static final int TYPE_GLOBAL		= 1;
	public static final int TYPE_FILE		= 2;
	public static final int TYPE_STRUCTURE	= 3;
	public static final int TYPE_FUNCTION	= 4;
	public static final int TYPE_BLOCK		= 5;

	public Scope(int type) {
		parent = null;
		this.type = type;
		symbolsByName = new HashMap<String,ArrayList<Symbol>>();
		symbolsByID = new HashMap<Long,Symbol>();
		observers = null;
		isNotificationEnabled = true;
	}
	public Scope(int type,Symbol[] symbols) {
		this(type);
		addAll(symbols);
	}
	public Scope(int type,Collection<Symbol> symbols) {
		this(type);
		addAll(symbols);
	}
	public void setParent(Scope parent) {
		this.parent = parent;
	}
	public Scope getParent() {
		return parent;
	}
	public int getType() {
		return type;
	}

	public synchronized int sizeLocal() {
		return symbolsByID.size();
	}
	public synchronized int size() {
		return symbolsByID.size()+((parent!=null)?parent.size():0);
	}

	public void add(Symbol symbol) {
		boolean notify = false;
		// NOTE: Don't keep the monitor while notifying
		synchronized (this) {
			if (!symbolsByID.containsKey(symbol.getID())) {
				NameLine name = symbol.getName();
				name.addObserver(new NameObserver(symbol));
				if (symbolsByName.containsKey(name.toString())) {
					symbolsByName.get(name.toString()).add(symbol);
				}
				else {
					ArrayList<Symbol> list = new ArrayList<Symbol>(1);	//Odds are that there'll only be one thing by this name in scope
					list.add(symbol);
					symbolsByName.put(name.toString(),list);
				}
				symbolsByID.put(symbol.getID(),symbol);
				notify = true;
			}
		}
		if (notify) {
			notifyObserversAdded(symbol);
		}
	}

	public synchronized void remove(Symbol symbol) {
		remove(symbol.getID());
	}
	public synchronized void remove(long id) {
		Symbol notifySymbol = null;
		// NOTE: Don't keep the monitor while notifying
		synchronized (this) {
			if (containsLocal(id)) {
				Symbol symbol = symbolsByID.get(id);
				symbolsByID.remove(id);
				ArrayList<SingleLine.LineObserver> observers = symbol.getName().getObservers();
				if (observers!=null) {
					for (int i=0;i<observers.size();++i) {
						if (observers.get(i) instanceof NameObserver) {
							observers.remove(i);
						}
					}
				}
				String name = symbol.getName().toString();
				ArrayList<Symbol> list = symbolsByName.get(name);
				if (list.size()==1) {
					symbolsByName.remove(name);
				}
				else {
					list.remove(symbol);
				}
				notifySymbol = symbol;
			}
			else if (parent!=null) {
				parent.remove(id);
			}
		}
		if (notifySymbol!=null) {
			notifyObserversRemoved(notifySymbol);
		}
	}

	public synchronized ArrayList<Symbol> getLocal(String name) {
		return symbolsByName.get(name);
	}
	public synchronized Symbol getLocal(long id) {
		return symbolsByID.get(id);
	}
	public synchronized ArrayList<Symbol> get(String name) {
		if (containsLocal(name)) {
			return getLocal(name);
		}
		if (parent!=null) {
			return parent.get(name);
		}
		return null;
	}
	public synchronized Symbol get(long id) {
		if (containsLocal(id)) {
			return getLocal(id);
		}
		if (parent!=null) {
			return parent.get(id);
		}
		return null;
	}

	public synchronized boolean containsLocal(String name) {
		return symbolsByName.containsKey(name);
	}
	public synchronized boolean contains(String name) {
		return containsLocal(name) || (parent!=null && parent.contains(name));
	}
	public synchronized boolean containsLocal(Symbol symbol) {
		return symbolsByID.containsKey(symbol.getID());
	}
	public synchronized boolean contains(Symbol symbol) {
		return contains(symbol.getID());
	}
	public synchronized boolean containsLocal(long id) {
		return symbolsByID.containsKey(id);
	}
	public synchronized boolean contains(long id) {
		return containsLocal(id) || (parent!=null && parent.contains(id));
	}

	public synchronized Symbol[] getAllLocal() {
		return symbolsByID.values().toArray(new Symbol[symbolsByID.size()]);
	}
	public synchronized Symbol[] getAll() {
		final int size = size();
		Symbol[] symbols = new Symbol[size];
		int i = 0;
		for (Symbol symbol : symbolsByID.values()) {
			symbols[i++] = symbol;
		}
		if (parent!=null) {
			Symbol[] symbols2 = parent.getAll();
			System.arraycopy(symbols2,0,symbols,i,symbols2.length);
		}
		return symbols;
	}
	public synchronized void addAll(Symbol[] symbols) {
		for (Symbol symbol : symbols) {
			add(symbol);
		}
	}
	public synchronized void addAll(Collection<? extends Symbol> symbols) {
		for (Symbol symbol : symbols) {
			add(symbol);
		}
	}

	public synchronized void removeAll(Collection<? extends Symbol> symbols) {
		for (Symbol symbol : symbols) {
			remove(symbol);
		}
	}

	public synchronized void addObserver(ScopeObserver observer) {
		if (observers==null) {
			observers = new ArrayList<ScopeObserver>();
		}
		synchronized (observers) {
			observers.add(observer);
		}
	}

	public synchronized void removeObserver(ScopeObserver observer) {
		if (observers!=null) {
			synchronized (observers) {
				observers.remove(observer);
			}
		}
	}

	private void notifyObserversAdded(Symbol symbol) {
		ArrayList<ScopeObserver> observers;
		boolean isNotificationEnabled;
		// NOTE: Don't keep the monitor while notifying people, since it could lead to deadlocks.
		synchronized (this) {
			observers = this.observers;
			isNotificationEnabled = this.isNotificationEnabled;
		}
		if (isNotificationEnabled && observers!=null) {
			ArrayList<ScopeObserver> observersCopy;
			synchronized (observers) {
				// Make a copy of the list of observers, to avoid deadlock.
				observersCopy = new ArrayList<ScopeObserver>(observers.size());
				observersCopy.addAll(observers);
			}
			for (ScopeObserver observer : observersCopy) {
				observer.symbolAdded(this,symbol);
			}
		}
	}
	private void notifyObserversRemoved(Symbol symbol) {
		ArrayList<ScopeObserver> observers;
		boolean isNotificationEnabled;
		// NOTE: Don't keep the monitor while notifying people, since it could lead to deadlocks.
		synchronized (this) {
			observers = this.observers;
			isNotificationEnabled = this.isNotificationEnabled;
		}
		if (isNotificationEnabled && observers!=null) {
			ArrayList<ScopeObserver> observersCopy;
			synchronized (observers) {
				// Make a copy of the list of observers, since observers can be removed as a result of this
				// notification, which screws up Java's iterator on the loop.
				// It also avoids deadlock.
				observersCopy = new ArrayList<ScopeObserver>(observers.size());
				observersCopy.addAll(observers);
			}
			for (ScopeObserver observer : observersCopy) {
				// Double-check to make sure that the observer is still observing before notifying.
				if (observers.contains(observer)) {
					observer.symbolRemoved(this,symbol);
				}
			}
			// Clear the copy to make garbage collection easier.
			observersCopy.clear();
		}
	}
	public void symbolChanged(SymbolChangeEvent e) {
		// NOTE: The notification below is not synchronized, so that the monitor is released, to avoid deadlocks
		synchronized (this) {
			if (e instanceof SymbolRenameEvent) {
				SymbolRenameEvent renaming = (SymbolRenameEvent)e;
				String oldName = renaming.getOldName();
				String newName = renaming.getNewName();
				ArrayList<Symbol> list = get(renaming.getOldName());
				if (list!=null) {
					for (Symbol symbol : list) {
						// Find the identical Symbol
						if (symbol==renaming.getSymbol()) {
							// Remove old name
							if (list.size()==1) {
								symbolsByName.remove(oldName);
							}
							else {
								list.remove(symbol);
							}
							// Add new name
							if (symbolsByName.containsKey(newName)) {
								symbolsByName.get(newName).add(symbol);
							}
							else {
								list = new ArrayList<Symbol>(1);	//Odds are that there'll only be one thing by this name in scope
								list.add(symbol);
								symbolsByName.put(newName,list);
							}
							break;
						}
					}
				}
			}
		}
		notifyObserversChanged(e);
	}
	private void notifyObserversChanged(SymbolChangeEvent e) {
		ArrayList<ScopeObserver> observers;
		boolean isNotificationEnabled;
		// NOTE: Don't keep the monitor while notifying people, since it could lead to deadlocks.
		synchronized (this) {
			observers = this.observers;
			isNotificationEnabled = this.isNotificationEnabled;
		}
		if (isNotificationEnabled && observers!=null) {
			ArrayList<ScopeObserver> observersCopy;
			synchronized (observers) {
				// Make a copy of the list of observers, to avoid deadlock.
				observersCopy = new ArrayList<ScopeObserver>(observers.size());
				observersCopy.addAll(observers);
			}
			for (ScopeObserver observer : observersCopy) {
				observer.symbolChanged(this,e);
			}
		}
	}

	/**
	 * This function must be called by all constructors of classes implementing Symbol to get a unique ID
	 * @return a unique ID
	 */
	public synchronized static long getUniqueID() {
		return NEXT_UNIQUE_ID++;
	}

	public synchronized void setNotificationEnabled(boolean isNotificationEnabled) {
		this.isNotificationEnabled = isNotificationEnabled;
	}

	public synchronized boolean isNotificationEnabled() {
		return isNotificationEnabled;
	}

	public static synchronized boolean isBeingRenamed(Symbol symbol) {
		return symbol!=null && beingRenamed==symbol;
	}
	public static synchronized Symbol getBeingRenamed() {
		return beingRenamed;
	}
	public static synchronized void addRenameReference(Symbol symbol,TextCaret caret) {
		if (isBeingRenamed(symbol)) {
			addedRenameReferences.add(caret);
		}
	}
	public static synchronized TextCaret[] getAddedRenameReferences(Symbol symbol) {
		return addedRenameReferences.size()>0 ? addedRenameReferences.toArray(new TextCaret[addedRenameReferences.size()]) : null;
	}

	private class NameObserver implements SingleLine.LineObserver {
		private Symbol symbol;
		private NameObserver(Symbol symbol) {
			this.symbol = symbol;
		}
		public void valueChanged(SingleLine s, String oldValue) {
			SymbolRenameEvent event = new SymbolRenameEvent(symbol,oldValue,s.toString());
			addedRenameReferences.clear();
			beingRenamed = symbol;
			symbolChanged(event);
			beingRenamed = null;
		}
	}
}
